package cn.sciento.starter.keyencrypt.mvc;

import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import javax.servlet.ServletRequest;
import javax.servlet.ServletRequestWrapper;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import cn.sciento.starter.keyencrypt.core.Encrypt;
import cn.sciento.starter.keyencrypt.core.EncryptContext;
import cn.sciento.starter.keyencrypt.core.IEncryptionService;
import cn.sciento.starter.keyencrypt.util.EncryptUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.util.Assert;
import org.springframework.web.bind.ServletRequestDataBinder;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor;

public class EncryptServletModelAttributeMethodProcessor extends ServletModelAttributeMethodProcessor {
  @Autowired
  IEncryptionService encryptionService;
  
  public EncryptServletModelAttributeMethodProcessor() {
    super(false);
  }
  
  public boolean supportsParameter(MethodParameter parameter) {
    return (parameter.hasParameterAnnotation(Encrypt.class) && 
      !BeanUtils.isSimpleProperty(parameter.getParameterType()));
  }
  
  protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
    if (!EncryptContext.isEncrypt()) {
      super.bindRequestParameters(binder, request);
      return;
    } 
    ServletRequest servletRequest = (ServletRequest)request.getNativeRequest(ServletRequest.class);
    Assert.state((servletRequest != null), "No ServletRequest");
    ServletRequestDataBinder servletBinder = (ServletRequestDataBinder)binder;
    Field[] fields = FieldUtils.getFieldsWithAnnotation(binder.getTarget().getClass(), Encrypt.class);
    if (ArrayUtils.isNotEmpty(fields)) {
      ParameterRequestWrapper requestWrapper = new ParameterRequestWrapper(servletRequest);
      String attr = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE;
      Map<String, String> uriVars = (Map<String, String>)servletRequest.getAttribute(attr);
      for (Field field : fields) {
        Encrypt encrypt = field.<Encrypt>getAnnotation(Encrypt.class);
        String encryptId = requestWrapper.getParameter(field.getName());
        if (StringUtils.isNotEmpty(encryptId)) {
          if (Collection.class.isAssignableFrom(field.getType()) || field.getType().isArray()) {
            requestWrapper.addParameter(field.getName(), Arrays.<String>stream(encryptId.split(","))
                .map(item -> EncryptUtils.ignoreValue(encrypt, item) ? item : this.encryptionService.decrypt(item, encrypt.value(), encrypt.ignoreUserConflict()))
                .collect(Collectors.joining(",")));
          } else {
            encryptId = EncryptUtils.ignoreValue(encrypt, encryptId) ? encryptId : this.encryptionService.decrypt(encryptId, encrypt.value(), encrypt.ignoreUserConflict());
            requestWrapper.addParameter(field.getName(), encryptId);
          } 
        } else if (uriVars != null && uriVars.containsKey(field.getName())) {
          encryptId = uriVars.get(field.getName());
          if (encryptId != null && (Collection.class.isAssignableFrom(field.getType()) || field.getType().isArray())) {
            uriVars.put(field.getName(), Arrays.<String>stream(encryptId.split(","))
                .map(item -> EncryptUtils.ignoreValue(encrypt, item) ? item : this.encryptionService.decrypt(item, encrypt.value(), encrypt.ignoreUserConflict()))
                .collect(Collectors.joining(",")));
          } else {
            encryptId = EncryptUtils.ignoreValue(encrypt, encryptId) ? encryptId : this.encryptionService.decrypt(encryptId, encrypt.value(), encrypt.ignoreUserConflict());
            uriVars.put(field.getName(), encryptId);
          } 
        } 
      } 
      servletBinder.bind((ServletRequest)requestWrapper);
    } else {
      servletBinder.bind(servletRequest);
    } 
  }
  
  public class ParameterRequestWrapper extends ServletRequestWrapper {
    private Map<String, String[]> params = (Map)new HashMap<>();
    
    public ParameterRequestWrapper(ServletRequest request) {
      super(request);
      this.params.putAll(request.getParameterMap());
    }
    
    public ParameterRequestWrapper(ServletRequest request, Map<String, Object> extendParams) {
      this(request);
      addAllParameters(extendParams);
    }
    
    public String getParameter(String name) {
      String[] values = this.params.get(name);
      if (values == null || values.length == 0)
        return null; 
      return values[0];
    }
    
    public String[] getParameterValues(String name) {
      return this.params.get(name);
    }
    
    public void addAllParameters(Map<String, Object> otherParams) {
      for (Map.Entry<String, Object> entry : otherParams.entrySet())
        addParameter(entry.getKey(), entry.getValue()); 
    }
    
    public void addParameter(String name, Object value) {
      if (value != null)
        if (value instanceof String[]) {
          this.params.put(name, (String[])value);
        } else if (value instanceof String) {
          this.params.put(name, new String[] { (String)value });
        } else {
          this.params.put(name, new String[] { String.valueOf(value) });
        }  
    }
  }
}
